import yaml
import argparse
import uuid
import os
import sys
import subprocess
import shutil

def mkdir_p(path):
    try:
        os.makedirs(path)
    except OSError:
        pass

class Image:
    def __init__(self, **kwargs):
        self.basepath = None
        self.parent = None
        self.config = {}

        for key, value in kwargs.iteritems():
            if key == "basepath":
                self.basepath = value
            if key == "config":
                self.config = value
            if key == "parent":
                self.parent = value

        if not self.basepath:
            raise ValueError("No basepath specified")

        if not self.config.has_key('repositories'):
            self.config['repositories'] = []

        self.dnf_dir = os.path.join(self.basepath, "dnf")
        self.installroot = os.path.join(self.basepath, "build")

        if self.parent:
                self.config['repositories'].extend(self.parent.config['repositories'])
                self.config['releasever'] = self.parent.config['releasever']

    def build(self):
        self.copy_parent()
        self.configure_dnf()
        self.install_packages()
        self.create_files()
        self.append_files()
        self.enable_units()
        self.run_scripts()

    def copy_parent(self):
        if not self.parent:
            return

        if os.path.exists(self.installroot):
            return

        shutil.copytree(self.parent.installroot, self.installroot, symlinks=True)

    def configure_dnf(self):
        mkdir_p(self.dnf_dir)
        configfile = "[main]\ngpgcheck=1\ninstallonly_limit=3\nclean_requirements_on_remove=True\nreposdir=do-not-use\n"

        with open(os.path.join(self.dnf_dir, 'dnf.conf'), 'w') as stream:
            stream.write(configfile)
            for repo in self.config['repositories']:
                stream.write(repo)

    def install_packages(self):
        try:
            if len(self.config['packages']) == 0:
                return
        except KeyError:
            return

        command = [ 'dnf', '-y', '-c', os.path.join(self.dnf_dir, 'dnf.conf'), "--releasever=%s" % self.config["releasever"], "--installroot=%s" % self.installroot, 'install' ]
        command.extend(self.config["packages"])

        retval = subprocess.call(command)
        if retval > 0:
            raise RuntimeError("DNF failed")

    def enable_units(self):
        try:
            if len(self.config['enableunits']) == 0:
                return
        except KeyError:
            return

        command = [ 'systemd-nspawn', '-D', self.installroot, 'systemctl', 'enable' ]
        command.extend(self.config['enableunits'])

        retval = subprocess.call(command)
        if retval > 0:
            raise RuntimeError("Failed to enable units")

    def create_files(self):
        try:
            if len(self.config['files']) == 0:
                return
        except KeyError:
            return

        for name, contents in self.config['files'].iteritems():
            mkdir_p(os.path.dirname(self.installroot + name))
            with open(self.installroot + name, 'w') as stream:
                stream.write(contents)

    def append_files(self):
        try:
            if len(self.config['appendfiles']) == 0:
                return
        except KeyError:
            return

        for name, contents in self.config['appendfiles'].iteritems():
            mkdir_p(os.path.dirname(self.installroot + name))
            with open(self.installroot + name, 'a') as stream:
                stream.write(contents)

    def run_scripts(self):
        try:
            if len(self.config['postscripts']) == 0:
                return
        except KeyError:
            return

        command = [ 'systemd-nspawn', '-D', self.installroot, 'sh', '/postscript' ]

        for script in self.config['postscripts']:
            with open(self.installroot + '/postscript', 'w') as stream:
                stream.write(script)
                
            retval = subprocess.call(command)
            os.remove(self.installroot + '/postscript')

            if retval > 0:
                raise RuntimeError("Failed to run postscript")

def main():
    parser = argparse.ArgumentParser(description='Build an nspawn image.')
    parser.add_argument('--basedir',
        dest = 'basedir',
        default = os.getcwd(),
        help = 'Base directory to build in (defaults to current directory)'
    )
    parser.add_argument('--configfile',
        dest = 'configfile',
        default = os.path.join(os.getcwd(), "images.yml"),
        help = 'Build definition file (defaults to images.yml)'
    )

    args = parser.parse_args()
    #session_uuid = uuid.uuid4()
    session_uuid = "build"
    session_dir = os.path.join(os.path.abspath(args.basedir), str(session_uuid))
    configfile = os.path.abspath(args.configfile)

    configdata = {}
    with open(configfile, 'r') as stream:
        try:
            configdata = yaml.load(stream)
        except yaml.YAMLError as exc:
            print(exc)
            sys.exit(1)

    images_ordered = []
    while True:
        unresolved = False
        for image in configdata:
            if configdata[image] in images_ordered:
                continue
            if configdata[image].has_key('baseimage'):
                if not configdata[configdata[image]['baseimage']] in images_ordered:
                    unresolved = True
                    continue

            configdata[image]['name'] = image
            images_ordered.append(configdata[image])
        if not unresolved:
            break

    mkdir_p(session_dir)

    images = {}
    for image in images_ordered:
        image_basepath = os.path.join(session_dir, image['name'])
        mkdir_p(image_basepath)
        if image.has_key('baseimage'):
            parent = images[image['baseimage']]
        else:
            parent = None
        i = Image(basepath = image_basepath, config = image, parent = parent)

        images[image['name']] = i
        print("Building %s" % image['name'])
        i.build()
        

if __name__ == "__main__":
    main()
